package com.onadake.domain.query;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;

import com.onadake.utils.DomainUtils;
import com.onadake.utils.ValidationUtils;

/**
 * Describes the generic filter.
 * Allows to specify multiple criteria which are connected by AND operator.
 * Allows to specify the ordering of the result set.
 * Allows to specify the from and limit values.
 * <p></p>
 * Ordering is specified according the list of order criteria.
 * If the ordering criteria are empty, then there is no specific requirement for that.
 * The most significant order criterium is the first one. If some group of records has not fully decided
 * order after applying the first order criterium, then this group is ordered by the second one.
 * Then the third...
 * 
 * @author radek.hecl
 *
 * @param <ColumnType> type of column for criterium and order
 */
public class Filter<ColumnType> {

    /**
     * Builder object.
     *
     * @param <ColumnType> type of column for criterium and order
     */
    public static class Builder<ColumnType> {

        /**
         * Criteria for selection.
         */
        private Set<Criterium<ColumnType>> criteria = new HashSet<Criterium<ColumnType>>();

        /**
         * Ordering criteria.
         */
        private List<Order<ColumnType>> orders = new ArrayList<Order<ColumnType>>();

        /**
         * From which element to select.
         */
        private long from = 0;

        /**
         * Limit of the selected elements, null for no limit.
         */
        private Integer limit = null;

        /**
         * Sets the specified criteria.
         * 
         * @param criteria criteria
         * @return this instance
         */
        public Builder<ColumnType> setCriteria(Collection<Criterium<ColumnType>> criteria) {
            this.criteria = DomainUtils.softCopySet(criteria);
            return this;
        }

        /**
         * Adds the specified criterium.
         * 
         * @param criterium criterium
         * @return this instance
         */
        public Builder<ColumnType> addCriterium(Criterium<ColumnType> criterium) {
            criteria.add(criterium);
            return this;
        }

        /**
         * Adds the specified in criterium.
         * 
         * @param column column
         * @param values collection with values
         * @return this instance
         */
        public Builder<ColumnType> addInCriterium(ColumnType column, Collection<?> values) {
            criteria.add(Criterium.createIn(column, values));
            return this;
        }

        /**
         * Adds the specified equal criterium.
         * 
         * @param column column
         * @param value value
         * @return this instance
         */
        public Builder<ColumnType> addEqualCriterium(ColumnType column, Object value) {
            criteria.add(Criterium.createEqual(column, value));
            return this;
        }

        /**
         * Adds the specified not equal criterium.
         * 
         * @param column column
         * @param value value
         * @return this instance
         */
        public Builder<ColumnType> addNotEqualCriterium(ColumnType column, Object value) {
            criteria.add(Criterium.createNotEqual(column, value));
            return this;
        }

        /**
         * Adds the specified greater criterium.
         * 
         * @param column column
         * @param value value
         * @return this instance
         */
        public Builder<ColumnType> addGreaterCriterium(ColumnType column, Object value) {
            criteria.add(Criterium.createGreater(column, value));
            return this;
        }

        /**
         * Adds the specified greater or equal criterium.
         * 
         * @param column column
         * @param value value
         * @return this instance
         */
        public Builder<ColumnType> addGreaterOrEqualCriterium(ColumnType column, Object value) {
            criteria.add(Criterium.createGreaterOrEqual(column, value));
            return this;
        }

        /**
         * Adds the specified less criterium.
         * 
         * @param column column
         * @param value value
         * @return this instance
         */
        public Builder<ColumnType> addLessCriterium(ColumnType column, Object value) {
            criteria.add(Criterium.createLess(column, value));
            return this;
        }

        /**
         * Adds the specified less or equal criterium.
         * 
         * @param column column
         * @param value value
         * @return this instance
         */
        public Builder<ColumnType> addLessOrEqualCriterium(ColumnType column, Object value) {
            criteria.add(Criterium.createLessOrEqual(column, value));
            return this;
        }

        /**
         * Adds the specified like criterium.
         * 
         * @param column column
         * @param value value
         * @return this instance
         */
        public Builder<ColumnType> addLikeCriterium(ColumnType column, Object value) {
            criteria.add(Criterium.createLike(column, value));
            return this;
        }
        
        /**
         * Adds the specified criteria.
         * 
         * @param criteria criteria
         * @return this instance
         */
        public Builder<ColumnType> addCriteria(Collection<Criterium<ColumnType>> criteria) {
            this.criteria.addAll(criteria);
            return this;
        }

        /**
         * Sets the specified order criteria.
         * 
         * @param orders order criteria
         * @return this instance
         */
        public Builder<ColumnType> setOrders(Collection<Order<ColumnType>> orders) {
            this.orders = DomainUtils.softCopyList(orders);
            return this;
        }

        /**
         * Adds the specified order criterium.
         * 
         * @param order order criterium
         * @return this instance
         */
        public Builder<ColumnType> addOrder(Order<ColumnType> order) {
            orders.add(order);
            return this;
        }

        /**
         * Adds the specified ascendant order criterium.
         * 
         * @param column for ordering
         * @return this instance
         */
        public Builder<ColumnType> addAscendantOrder(ColumnType column) {
            orders.add(Order.createAscendant(column));
            return this;
        }

        /**
         * Adds the specified descendant order criterium.
         * 
         * @param column for ordering
         * @return this instance
         */
        public Builder<ColumnType> addDescendantOrder(ColumnType column) {
            orders.add(Order.createDescendant(column));
            return this;
        }

        /**
         * Adds the specified order criteria.
         * 
         * @param orders order criteria
         * @return this instance
         */
        public Builder<ColumnType> addOrders(Collection<Order<ColumnType>> orders) {
            this.orders.addAll(orders);
            return this;
        }

        /**
         * Sets the from.
         * 
         * @param from from
         * @return this instance
         */
        public Builder<ColumnType> setFrom(long from) {
            this.from = from;
            return this;
        }

        /**
         * Sets the limit for selection. Null means unlimited.
         * 
         * @param limit limit for selection
         * @return this instance
         */
        public Builder<ColumnType> setLimit(Integer limit) {
            this.limit = limit;
            return this;
        }

        /**
         * Sets the source filter.
         * 
         * @param source source filter
         * @return this instance
         */
        public Builder<ColumnType> setSource(Filter<ColumnType> source) {
            setCriteria(source.criteria);
            setOrders(source.orders);
            setFrom(source.from);
            setLimit(source.limit);
            return this;
        }

        /**
         * Builds the result object.
         * 
         * @return created object
         */
        public Filter<ColumnType> build() {
            return new Filter<ColumnType>(this);
        }
    }

    /**
     * Criteria for selection.
     */
    private Set<Criterium<ColumnType>> criteria;

    /**
     * Ordering criteria.
     */
    private List<Order<ColumnType>> orders;

    /**
     * From which element to select.
     */
    private long from;

    /**
     * Limit of the selected elements, null for no limit.
     */
    private Integer limit;

    /**
     * Creates new instance.
     * 
     * @param builder builder object
     */
    public Filter(Builder<ColumnType> builder) {
        criteria = Collections.unmodifiableSet(DomainUtils.softCopySet(builder.criteria));
        orders = Collections.unmodifiableList(DomainUtils.softCopyList(builder.orders));
        from = builder.from;
        limit = builder.limit;
        guardInvariants();
    }

    /**
     * Guards this object to be consistent. Throws exception if this is not the case.
     */
    private void guardInvariants() {
        ValidationUtils.guardNotNullCollection(criteria, "criteria cannot have null");
        ValidationUtils.guardNotNullCollection(orders, "orders cannot have null");
        ValidationUtils.guardNotNegativeLong(from, "from must be not negative");
        if (limit != null) {
            ValidationUtils.guardNotNegativeInt(limit, "limit must be null or not negative");
        }
    }

    /**
     * Returns criteria which should be connected by AND filter.
     * 
     * @return criteria
     */
    public Set<Criterium<ColumnType>> getCriteria() {
        return criteria;
    }

    /**
     * Returns ordering from the most significant.
     * 
     * @return ordering
     */
    public List<Order<ColumnType>> getOrders() {
        return orders;
    }

    /**
     * Returns from which element make the selection.
     * 
     * @return from which element make the selection
     */
    public long getFrom() {
        return from;
    }

    /**
     * Returns the limit for selection. Null means unlimited.
     * 
     * @return limit for selection
     */
    public Integer getLimit() {
        return limit;
    }

    @Override
    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this);
    }

    @Override
    public boolean equals(Object obj) {
        return EqualsBuilder.reflectionEquals(this, obj);
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this);
    }
}
